Skip to main content

Spring 手撕 IOC

提到 spring 不可避免的就是两个核心组件 IOC 和 AOP 这里我们主要实现的是 IOC

那接触到这个实现 ioc 的文章 适合哪些人群一起学习呢

  • 了解反射
  • 至少 用过 spring 或多或少能感受到 IOC 给我们带来的好处

IOC (inverse of control)

简单介绍

IOC 就是将创建对象的权限,从 Java 程序员 交给 IOC 容器来创建,就是将对象的创造全 给到了框架,

我们称为 控制反转

创建一个 Maven 项目

我们用 maven 项目来演示和编写 spring 的 ioc 容器代码

1、配置依赖

pom.xml

    <dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>

2、编写三层架构

模拟数据 DAO

public class hellloDaoImpl implements helloDao {

@Override
public List<String> findAll() {
return Arrays.asList("1", "2", "3");
}
}

连接视图和数据层: service

public class helloServiceImpl implements helloservice {
private helloDao helloDao = new hellloDaoImpl();

@Override
public List<String> findAll() {
return helloDao.findAll();
}
}

控制层 : servlet/controller

@WebServlet("/hello")
public class helloservlet extends HttpServlet {

helloservice helloservice = new helloServiceImpl();

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("spring" + helloservice.findAll());
}
}

3、配置 Tomcat 部署

image-20220324162724351

需要解决的问题

不同实现需要重写接口代码

DAO 操作的是数据源, 数据源(数据库) 如果此时项目变更,比如数据库变成 Oracle 那么我们的 DAO 代码可能就需要推翻重写

如何解决呢?

设计模式

使用静态工厂来创建特定的类, 不再把代码写死在 service 中,通过工厂的方式来实现

public class beanfactory {
public static helloDao getDao() {
return new hellloDaoImpl();
}
}

service 代码

  private helloDao helloDao = beanfactory.getDao();

上述方法提供的思路 还是不能解决我们的问题,需求发生改变的时候 , 仍然需要改变代码 ,这个时候我们就要用到反射 如果不修改 java 代码 如何实现类的切换呢?

使用 : 外部配置文件的方式 将具体的类写到配置文件中, Java 程序只需要读取配置文件即可

编写外部文件

properties , yml , xml 等配置文件的方式来读取需要根绝什么数据源来使用什么工厂、这里我们使用 properties

1、定义外部配置文件

hellDao=com.hyc.Dao.Impl.hellloDaoImpl

2、Java 程序读取这个配置文件

    private static Properties properties;
static {
properties = new Properties();
try {
properties.load(beanfactory.class.getClassLoader().getResourceAsStream("factory.properties"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static Object getDao() {
String helloDao = properties.getProperty("helloDao");
//利用反射机制创建对象
try {

Class clazz = Class.forName(helloDao);
Object object = clazz.getConstructor(null).newInstance();
return object;
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}

3、修改 service

  private helloDao helloDao = (helloDao) beanfactory.getDao();

测试

正常来说 我们访问 hello 会返回 123 (再 dao 中模拟的数据),这个时候我们需要切换数据源只需要修改外部配置文件即可

image-20220324182729546

此时我们修改外部配置文件

helloDao=com.hyc.Dao.Impl.hellloDaoImpl2

hellloDaoImpl2 中模拟的数据是 7,8,9 ,重启项目

image-20220324183058540

到这里我们就使用外部配置+工厂的方法解决了代码不能切换的问题,

创建出来的对象并非单例

这个问题会出现什么问题呢 重复使用对象的话会出现相同的对象创造很多个无用实例的问题

这里我们就提出用缓存的思路来保证单例,

  • 用 Map 来存放创建的对象,避免重复创建
  • 双重判断保证线程安全

修改工厂生成对象的代码

    public static Object getDao(String beanName) {
if (!cache.containsKey(beanName)) {
synchronized (beanfactory.class) {
if (!cache.containsKey(beanName)) {
//利用反射机制创建对象
//将对象放入我们的缓存里(map)
try {
String helloDao = properties.getProperty("helloDao");
Class clazz = Class.forName(helloDao);
Object object = clazz.getConstructor(null).newInstance();
cache.put(beanName, object);
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
}
return cache.get(beanName);
}

测试

    public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
helloDao helloDao = (helloDao) beanfactory.getDao("helloDao");
System.out.println(helloDao);
}
}

image-20220324184054986

这样我们就实现了对工厂生成的实例复用,而并非每次都重新创建一个

两种方式的区别

private helloDao hellodao = new hellodaoimpl();

private helloDao hellodao = (helloDao) BeanFactory.getDao("helloDao");
  1. 强依赖/紧耦合 编译之后无法修改 没有拓展性
  2. 弱依赖/松耦合 编译之后可以通过修改配置文件来变动,让程序具有更好的拓展性

我们自己放弃了创造对象的权限,将创建对象的权限交给了 BeanFactory 这种将控制权交给别人或者是对象的思想

就是 : IOC 控制反转

IOC 基于注解的执行原理和实现

手写基于注解的 Ioc 思路

  1. 自定义 MyAnnotationConfigApplicationContext ,构造器中传入要扫描的包
  2. 获取包下所有类
  3. 遍历这些类找到添加了 @Component 注解的类,获取它的 class 和对应的 beanName 封装成 beanDefinition 存入 Set 集合 这个机会就是 IOC 自动装载的原材料
  4. 遍历 Set 集合 通过反射机制创建对象 同时检测属性有没有添加 @ Value 注解 如果有给属性赋值,再将这些动态创建的对象以 K-V 的形式 放入缓存区
  5. 提供 getBean 方法 通过 beanName 取出对应的 bean 即可

根据上述的思路我们实现代码

注解

自动装配注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Autowired {
}

组件注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
String value() default "";
}

指定 Name 注入注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Qualifer {
String value();
}

赋值注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Value {
String value();
}

核心上下文

扫描包中组件类

public class MyTools {

public static Set<Class<?>> getClasses(String pack) {

// 第一个class类的集合
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
// 是否循环迭代
boolean recursive = true;
// 获取包的名字 并进行替换
String packageName = pack;
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
// 循环迭代下去
while (dirs.hasMoreElements()) {
// 获取下一个元素
URL url = dirs.nextElement();
// 得到协议的名称
String protocol = url.getProtocol();
// 如果是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
// 获取包的物理路径
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件 并添加到集合中
findClassesInPackageByFile(packageName, filePath, recursive, classes);
} else if ("jar".equals(protocol)) {
// 如果是jar包文件
// 定义一个JarFile
System.out.println("jar类型的扫描");
JarFile jar;
try {
// 获取jar
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 从此jar包 得到一个枚举类
Enumeration<JarEntry> entries = jar.entries();
findClassesInPackageByJar(packageName, entries, packageDirName, recursive, classes);
} catch (IOException e) {
// log.error("在扫描用户定义视图时从jar包获取文件出错");
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}

private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, final boolean recursive, Set<Class<?>> classes) {
// 同样的进行循环迭代
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 可以是目录 和一些jar包里的其他文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 如果是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 如果前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 如果以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx).replace('/', '.');
}
// 如果可以迭代下去 并且是一个包
if ((idx != -1) || recursive) {
// 如果是一个.class文件 而且不是目录
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// .error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}
}
}

private static void findClassesInPackageByFile(String packageName, String packagePath, final boolean recursive, Set<Class<?>> classes) {
// 获取此包的目录 建立一个File
File dir = new File(packagePath);
// 如果不存在或者 也不是目录就直接返回
if (!dir.exists() || !dir.isDirectory()) {
// log.warn("用户定义包名 " + packageName + " 下没有任何文件");
return;
}
// 如果存在 就获取包下的所有文件 包括目录
File[] dirfiles = dir.listFiles(new FileFilter() {
// 自定义过滤规则 如果可以循环(包含子目录) 或则是以.class结尾的文件(编译好的java类文件)
@Override
public boolean accept(File file) {
return (recursive && file.isDirectory()) || (file.getName().endsWith(".class"));
}
});
// 循环所有文件
for (File file : dirfiles) {
// 如果是目录 则继续扫描
if (file.isDirectory()) {
findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);
} else {
// 如果是java类文件 去掉后面的.class 只留下类名
String className = file.getName().substring(0, file.getName().length() - 6);
try {
// 添加到集合中去
// classes.add(Class.forName(packageName + '.' +
// className));
// 经过回复同学的提醒,这里用forName有一些不好,会触发static方法,没有使用classLoader的load干净
classes.add(Thread.currentThread().getContextClassLoader().loadClass(packageName + '.' + className));
} catch (ClassNotFoundException e) {
// log.error("添加用户自定义视图类错误 找不到此类的.class文件");
e.printStackTrace();
}
}
}
}

}


容器上下文代码

  • findBeanDefinitions 扫描包中符合条件的类
  • createObject 根据上个方法获取到的类的元数据信息创造对象,加入缓存
  • autowireObject 是否有需要自动注入的对象,判断注解存在决定 ByName 或者 ByType
/**
* @projectName: SpringIOC
* @package: com.hyc.MySpring
* @className: MyAnnotationConfigApplicationContext
* @author: 冷环渊 doomwatcher
* @description: TODO
* @date: 2022/3/24 18:53
* @version: 1.0
*/
public class MyAnnotationConfigApplicationContext {

private Map<String, Object> ioc = new HashMap<>();

public MyAnnotationConfigApplicationContext(String pack) {
//扫描包中的目标类
Set<BeanDefinition> beanDefinitions = findBeanDefinitions(pack);
//根据原材料 创建bean
createObject(beanDefinitions);
// 自动装载
autowireObject(beanDefinitions);
}

public void autowireObject(Set<BeanDefinition> beanDefinitions) {
//获取注入的集合
Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
while (iterator.hasNext()) {
//拿到bean 信息
BeanDefinition beanDefinition = iterator.next();
//获取当前需要自动装配的对象
Class clazz = beanDefinition.getBeanClass();
//拿到全部属性
Field[] declaredFields = clazz.getDeclaredFields();
for (Field field : declaredFields) {
Autowired annotation = field.getAnnotation(Autowired.class);
if (annotation != null) {
Qualifer qualifer = field.getAnnotation(Qualifer.class);
if (qualifer != null) {
try {
//byName
String beanName = qualifer.value();
Object bean = getBean(beanName);
String fieldName = field.getName();
String mothedName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
Method method = clazz.getMethod(mothedName, field.getType());
Object object = getBean(beanDefinition.getBeanName());
System.out.println(bean);
method.invoke(object, bean);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
} else {
//byType
}
}
}
}
}

public Object getBean(String beanName) {
return ioc.get(beanName);
}

public void createObject(Set<BeanDefinition> beanDefinitions) {
Iterator<BeanDefinition> iterator = beanDefinitions.iterator();
while (iterator.hasNext()) {
BeanDefinition beanDefinition = iterator.next();
Class clazz = beanDefinition.getBeanClass();
String beanName = beanDefinition.getBeanName();
try {
//创造对象
Object objects = clazz.getConstructor().newInstance();
//完成属性的赋值
Field[] declaredFields = clazz.getDeclaredFields();
for (Field declaredField : declaredFields) {
Value valueAnnotation = declaredField.getAnnotation(Value.class);
if (valueAnnotation != null) {
String value = valueAnnotation.value();
String fieldName = declaredField.getName();
String methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
Method method = clazz.getMethod(methodName, declaredField.getType());
//数据类型转换
Object val = null;
//可能会有很多类型 篇幅过长
switch (declaredField.getType().getName()) {
case "java.lang.Integer":
val = Integer.parseInt(value);
break;
case "java.lang.String":
val = value;
break;
case "java.lang.Float":
val = Float.parseFloat(value);
break;
}
method.invoke(objects, val);
}
}
ioc.put(beanName, objects);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}

}
}

public Set<BeanDefinition> findBeanDefinitions(String pack) {
// 1 获取包下的所有类
Set<Class<?>> classes = MyTools.getClasses(pack);
Iterator<Class<?>> iterator = classes.iterator();
Set<BeanDefinition> BeanDefinitions = new HashSet<>();
while (iterator.hasNext()) {
// 2,遍历这些类 找到添加了注解的类
Class<?> clazz = iterator.next();
Component annotation = clazz.getAnnotation(Component.class);
if (annotation != null) {
// 获取Compoent的值
String beanName = annotation.value();
if ("".equals(beanName)) {
// 获取类名首字母小写、
String className = clazz.getSimpleName();
beanName = className.substring(0, 1).toLowerCase() + className.substring(1, className.length());
}
BeanDefinition beanDefinition = new BeanDefinition(beanName, clazz);
BeanDefinitions.add(beanDefinition);
}
}
// 3. 将这些类封装成BeanDefinition 装载到集合中
return BeanDefinitions;
}
}

元数据

@Data
@AllArgsConstructor
public class BeanDefinition {
private String BeanName;
private Class BeanClass;
}

测试对象

Account

@Component()
@Data
public class Account {
public Account() {
}

@Value("1")
private Integer id;
@Value("张三")
private String name;
@Value("12")
private Integer age;
@Autowired
@Qualifer("myOrder")
private Order myorder;
}

Orderj

@Component("myOrder")
@Data
public class Order {
@Value("xxx123")
private String orderId;
@Value("1000.5")
private Float price;
}

总结

手动实现基于注解 Ioc 这篇文章,对于理解 spring 的装配过程 ,Bean 的声明周期 有着非常好的启发作用

手动实现过之后会才会知道为什么要解决这个问题,怎么解决这个问题

得到的成果

  1. 一个自己编写的 IOC 容器
  2. 反射技术的实战
  3. spring 原理的加深理解